/*******************************************************************************
 * Copyright (c) 2001, 2002 Boris Pruessmann and others. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Common Public License v0.5 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/cpl-v05.html
 *
 * Contributors: Boris Pruessmann - Initial implementation. David Corbin - added
 * images, MicroPlugin implementation, static getClientName() method
 ******************************************************************************/
package net.sourceforge.perforce.ui;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.core.PerforceException;
import net.sourceforge.perforce.core.PerforceSynchronizer;
import net.sourceforge.perforce.core.internal.api.PerforceServer;
import net.sourceforge.perforce.core.internal.events.AddDeleteMoveListener;
import net.sourceforge.perforce.core.resources.RemoteFile;
import net.sourceforge.perforce.core.resources.RemoteFolder;
import net.sourceforge.perforce.ui.action.PerforceAction;
import net.sourceforge.perforce.ui.internal.Policy;
import net.sourceforge.perforce.ui.internal.model.P4File;
import net.sourceforge.perforce.ui.model.PerforceAdapterFactory;
import net.sourceforge.perforce.ui.views.Console;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.ui.TeamUI;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorRegistry;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.actions.WorkspaceModifyDelegatingOperation;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;

/**
 * @version $Revision: 494 $
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 */
public class PerforceUIPlugin extends AbstractUIPlugin implements MicroPlugin,
    IPropertyChangeListener, IPerforceUIConstants {
  //----------------------------------------------------------------------------
  // Public Constants

  public static final String CALCULATE_DIRTY_PREFERENCE = ID + ".preferences.calculate_dirty"; //$NON-NLS-1$
  public static final String DELETE_AFFECTS_CHANGELIST =
      ID + ".preferences.delete_affects_changelist"; //$NON-NLS-1$

  public static final String ICON_PATH_FULL = "icons/full/"; //$NON-NLS-1$
  public static final String IMG_CLEAR = "clcl16/clear_co.gif"; //$NON-NLS-1$
  public static final String IMG_ROOTELEMENT = "clcl16/depot.gif"; //$NON-NLS-1$
  public static final String IMG_REPOSITORY = "cview16/depot_view.gif"; //$NON-NLS-1$
  public static final String IMG_ERROR = "misc/error.gif"; //$NON-NLS-1$
  public static final String IMG_SUBMITTED_CHANGELIST = "clcl16/submitted_cl.gif"; //$NON-NLS-1$

  private IPreSubmitOperation[] preSubmitOperations;
  // --------------------------------------------------------------------
  // Private Static Variables

  private static PerforceUIPlugin instance;

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Map containing preloaded ImageDescriptors */
  private Map<String, ImageDescriptor> imageDescriptors = Maps.newHashMapWithExpectedSize(24);

  // -------------------------------------------------------------------------
  // Public Constructors

  /** Constructor for PerforceUIPlugin. */
  public PerforceUIPlugin() {
  }

  // -----------------------------------------------------------------------
  // Public Static Methods

  /**
   * Returns the plugin's singleton instance.
   *
   * @return the singleton instance.
   */
  public static PerforceUIPlugin getPlugin() {
    Policy.assertNotNull(instance);
    return instance;
  }

  /**
   * Call this method to retrieve the currently active Workbench page.
   *
   * @return the active workbench page.
   */
  public static IWorkbenchPage getActivePage() {
    IWorkbenchWindow window = getPlugin().getWorkbench().getActiveWorkbenchWindow();
    if (window == null) {
      return null;
    } else {
      return window.getActivePage();
    }
  }

  /**
   * Call this method to retrieve the shell of the actie workbench window.
   *
   * @return the active shell.
   */
  public static Shell getActiveShell() {
    IWorkbenchWindow window = getPlugin().getWorkbench().getActiveWorkbenchWindow();
    if (window == null) {
      return null;
    } else {
      return window.getShell();
    }
  }

  /**
   * Returns the workspace instance.
   *
   * @return the workspace.
   */
  public static IWorkspace getWorkspace() {
    return ResourcesPlugin.getWorkspace();
  }

  /**
   * Call this method to log the given status.
   *
   * @param status the status to log.
   */
  public static void log(IStatus status) {
    getPlugin().getLog().log(status);
  }

  /**
   * Call this method to log an error with the given message & throwable.
   *
   * @param message the message.
   * @param throwable the exception.
   */
  public static void logError(String message, Throwable throwable) {
    getPlugin().getLog().log(
        new Status(IStatus.ERROR, PerforceUIPlugin.ID, IStatus.ERROR, message, throwable));
  }

  //----------------------------------------------------------------------------
  // -- Public Methods

  @Override
  public void start(BundleContext context) throws Exception {
    super.start(context);

    Policy.assertNull(instance);
    instance = this;

    getPreferenceStore().addPropertyChangeListener(this);

    Policy.localize("net.sourceforge.perforce.ui.messages"); //$NON-NLS-1$


    IStatus status = PerforceSynchronizer.getInstance().getStatus();
    if (!status.isOK()) {
      ErrorDialog.openError(null, Policy.bind("PerforceUIPlugin.synchronizerErrorTitle"), //$NON-NLS-1$
          Policy.bind("PerforceUIPlugin.synchronizerError"), //$NON-NLS-1$
          status, IStatus.INFO | IStatus.ERROR);
    }


    PerforceAdapterFactory factory = new PerforceAdapterFactory();
    Platform.getAdapterManager().registerAdapters(factory, PerforceServer.class);
    Platform.getAdapterManager().registerAdapters(factory, RemoteFolder.class);
    Platform.getAdapterManager().registerAdapters(factory, RemoteFile.class);
    Platform.getAdapterManager().registerAdapters(factory, P4File.class);

    initializeImages();
    initializeCorePreferences();

    Console.startup();

    TeamUI.addPropertyChangeListener(this);
  }

  @Override
  public void stop(BundleContext context) throws Exception {
    TeamUI.removePropertyChangeListener(this);

    Console.shutdown();
    PerforceDecorator.shutdown();

    super.stop(context);
  }

  /**
   * Call this method to retrieve the (cache) ImageDescriptor for the given id.
   *
   * @param id the id of the image descriptor.
   * @return the ImageDescriptor instance.
   */
  public ImageDescriptor getImageDescriptor(String id) {
    return imageDescriptors.get(id);
  }

  /**
   * Called to retrieve the workspace root.
   *
   * @return the workspace root.
   */
  public IWorkspaceRoot getWorkspaceRoot() {
    return ResourcesPlugin.getWorkspace().getRoot();
  }

  /** @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(PropertyChangeEvent) */
  public void propertyChange(PropertyChangeEvent event) {
    try {
      String property = event.getProperty();
      if (property.equals(PREF_SHOW_TASKS_ON_ADD)) {
        boolean b = ((Boolean) event.getNewValue()).booleanValue();
        PerforceCorePlugin.getPlugin().setShowTasksOnAdd(b);

        if (b) {
          AddDeleteMoveListener.refreshAllMarkers();
        } else {
          AddDeleteMoveListener.clearAllPerforceMarkers();
        }
      } else if (property.equals(DELETE_AFFECTS_CHANGELIST)) {
        boolean b = ((Boolean) event.getNewValue()).booleanValue();
        PerforceCorePlugin.getPlugin().setDeleteAffectsChangelist(b);
      } else if (property.equals(PREF_BACKGROUND_UPDATE_INTERVAL)) {
        int value = ((Integer) event.getNewValue()).intValue();
        PerforceCorePlugin.getPlugin().setBackgroundUpdateInterval(value);
      } else if (property.equals(PREF_SHOULD_MONITOR_CHANGELISTS)) {
        boolean b = ((Boolean) event.getNewValue()).booleanValue();
        PerforceCorePlugin.getPlugin().setShouldMonitorChangelists(b);
      } else if (property.equals(PREF_SHOULD_MONITOR_OPENED_FILES)) {
        boolean b = ((Boolean) event.getNewValue()).booleanValue();
        PerforceCorePlugin.getPlugin().setShouldMonitorOpenedFiles(b);
      } else if (property.equals(TeamUI.GLOBAL_IGNORES_CHANGED)) {
        if (PerforceCorePlugin.getPlugin().getShowTasksOnAdd()) {
          AddDeleteMoveListener.refreshAllMarkers();
        } else {
          AddDeleteMoveListener.clearAllPerforceMarkers();
        }
      }
    } catch (CoreException e) {
      log(e.getStatus());
    }
  }

  // ---------------------------------------------------------------------------
  // Protected Methods

  @Override
  protected void initializeDefaultPreferences(IPreferenceStore store) {
    store.setDefault(CALCULATE_DIRTY_PREFERENCE, false);
    store.setDefault(DELETE_AFFECTS_CHANGELIST, true);

    store.setDefault(PREF_DIRTY_FLAG, P4DecoratorConfiguration.DEFAULT_DIRTY_FLAG);
    store.setDefault(PREF_ADDED_FLAG, P4DecoratorConfiguration.DEFAULT_ADDED_FLAG);
    store.setDefault(PREF_FILETEXTFORMAT, P4DecoratorConfiguration.DEFAULT_FILETEXTFORMAT);
    store.setDefault(PREF_FOLDERTEXTFORMAT, P4DecoratorConfiguration.DEFAULT_FOLDERTEXTFORMAT);
    store.setDefault(PREF_PROJECTTEXTFORMAT, P4DecoratorConfiguration.DEFAULT_PROJECTTEXTFORMAT);
    store.setDefault(PREF_SHOW_TASKS_ON_ADD, true);
    store.setDefault(PREF_DONT_PROMPT_AUTOMATIC_OPEN, false);
    store.setDefault(PREF_WARN_ON_OTHER_OPEN, false);
    store.setDefault(PREF_CHECK_CHANGELIST_FOR_WARNINGS_AND_ERROR, true);
    store.setDefault(PREF_CHECK_FOR_NEWFILES_BEFORE_SUBMIT, false);
    store.setDefault(PREF_SHOW_FULLPATH_FOR_PROJECTS, false);
    store.setDefault(PREF_INDICATE_HAS_REMOTE, true);
    store.setDefault(PREF_INDICATE_NEW_RESOURCE, true);
    store.setDefault(PREF_INDICATE_OPENED_BY_OTHER, true);
    store.setDefault(PREF_INDICATE_OPENED_FOR_ADD, true);
    store.setDefault(PREF_INDICATE_OPENED_FOR_EDIT, true);

    store.setDefault(PREF_UNSAVED_RESOURCES_BEHAVIOR, URB_PROMPT);
    // Background Update Defaults
    store.setDefault(PREF_BACKGROUND_UPDATE_INTERVAL, 240);
    store.setDefault(PREF_SHOULD_MONITOR_CHANGELISTS, true);
    store.setDefault(PREF_SHOULD_MONITOR_OPENED_FILES, true);
  }

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Called to initialize the image cache. */
  private void initializeImages() {
    URL baseURL = getDescriptor().getInstallURL();

    createImageDescriptor(IMG_CLEAR, baseURL);
    createImageDescriptor(IMG_ROOTELEMENT, baseURL);
    createImageDescriptor(IMG_ERROR, baseURL);
    createImageDescriptor(IMG_REPOSITORY, baseURL);
    createImageDescriptor(IPerforceUIConstants.IMG_REFRESH, baseURL);


    createImageDescriptor(IPerforceUIConstants.IMG_CLIENT_CHANGELISTS, baseURL);
    createImageDescriptor(IPerforceUIConstants.IMG_CLIENT_CHANGELIST, baseURL);
    createImageDescriptor(IPerforceUIConstants.IMG_OTHER_CHANGELISTS, baseURL);
    createImageDescriptor(IPerforceUIConstants.IMG_OTHER_CHANGELIST, baseURL);
    createImageDescriptor(IMG_SUBMITTED_CHANGELIST, baseURL);

    createImageDescriptor(IPerforceUIConstants.IMG_PERFORCE_SERVER, baseURL);
    createImageDescriptor(IPerforceUIConstants.IMG_HEAD_REV, baseURL);
    createImageDescriptor(IPerforceUIConstants.IMG_NEWLOCATION, baseURL);
    createImageDescriptor(IPerforceUIConstants.IMG_WIZBAN_SHARE, baseURL);

    createImageDescriptor(OVR_HASREMOTE, baseURL);
    createImageDescriptor(OVR_NO_REMOTE, baseURL);
    createImageDescriptor(OVR_OUTOFDATE, baseURL);
    createImageDescriptor(OVR_OPENFORADD, baseURL);
    createImageDescriptor(OVR_OPENFOREDIT, baseURL);
    createImageDescriptor(OVR_OPENFORDELETE, baseURL);
    createImageDescriptor(OVR_LOCKED, baseURL);

    createImageDescriptor(OVR_OTHER_OPENFORADD, baseURL);
    createImageDescriptor(OVR_OTHER_OPENFORDELETE, baseURL);
    createImageDescriptor(OVR_OTHER_OPENFOREDIT, baseURL);
  }

  /**
   * Called to cache the image descriptor for th given id and baseUrl.
   *
   * @param id the id of the image.
   * @param baseURL the base url
   */
  private void createImageDescriptor(String id, URL baseURL) {
    URL url = null;
    try {
      url = new URL(baseURL, ICON_PATH_FULL + id);
    } catch (MalformedURLException e) {
      // Ignored!
    }

    ImageDescriptor desc = ImageDescriptor.createFromURL(url);
    imageDescriptors.put(id, desc);
  }

  /** Propagates the preferences to the core plugin. */
  private void initializeCorePreferences() {
    IPreferenceStore store = getPreferenceStore();

    PerforceCorePlugin corePlugin = PerforceCorePlugin.getPlugin();
    corePlugin.setShowTasksOnAdd(store.getBoolean(PREF_SHOW_TASKS_ON_ADD));
    corePlugin.setDeleteAffectsChangelist(store.getBoolean(DELETE_AFFECTS_CHANGELIST));

    corePlugin.setBackgroundUpdateInterval(store.getInt(PREF_BACKGROUND_UPDATE_INTERVAL));
    corePlugin.setShouldMonitorChangelists(store.getBoolean(PREF_SHOULD_MONITOR_CHANGELISTS));
    corePlugin.setShouldMonitorOpenedFiles(store.getBoolean(PREF_SHOULD_MONITOR_OPENED_FILES));
  }

  public static void openEditor(IFile file) {
    IWorkbench workbench = getPlugin().getWorkbench();
    IEditorRegistry registry = workbench.getEditorRegistry();
    IWorkbenchPage page = workbench.getActiveWorkbenchWindow().getActivePage();

    IEditorDescriptor descriptor = getDefaultEditor(file, registry);

    if (descriptor == null) return;

    try {
      page.openEditor(new FileEditorInput(file), editorId(descriptor));
    } catch (PartInitException e) {
      log(e.getStatus());
    }
  }

  private static IEditorDescriptor getDefaultEditor(IFile file, IEditorRegistry registry) {
    System.err.println("getDefaultEditor");
    try {
      return registry.getDefaultEditor(file.getName());
    } catch (Exception ignoredForEclipse2x) {
      System.err.println("caught exceptoin");
      if (!(ignoredForEclipse2x instanceof NoSuchMethodException)) {
        System.err.println("caught exceptoin");
        logError("Unable to getDefaultEditor", ignoredForEclipse2x);
        return null;
      }
    }
    try {
      //
      // Original method converted to Reflection for 2.x/3.x cross compatibility
      // IEditorDescriptor descriptor = registry.getDefaultEditor(file);
      //
      Method getDefaultEditorMethod =
          registry.getClass().getMethod("getDefaultEditor", new Class[] {IFile.class});
      if (getDefaultEditorMethod == null) {
        logError("Unable to find IEditorRegister#getDefaultEditor(IFile)", null);
        return null;
      }
      IEditorDescriptor descriptor =
          (IEditorDescriptor) getDefaultEditorMethod.invoke(registry, new Object[] {file});
      logError("Descriptor", null);

      return descriptor;
    } catch (Exception e) {
      logError("Unable to open editor", e);
      return null;
    }
  }

  private static String editorId(IEditorDescriptor descriptor) {
    String editorId;
    if (descriptor == null) {
      editorId = "org.eclipse.ui.DefaultTextEditor"; //$NON-NLS-1$
    } else {
      editorId = descriptor.getId();
    }
    return editorId;
  }

  public synchronized IPreSubmitOperation[] getPreSubmitOperations() {
    if (preSubmitOperations == null) {
      preSubmitOperations = computePreSubmitOperations();
    }

    return preSubmitOperations;
  }

  /**
   * Called to compute the preSubmitOperations
   *
   * @return array of preSubmitOperations
   */
  private IPreSubmitOperation[] computePreSubmitOperations() {
    IExtensionRegistry registry = Platform.getExtensionRegistry();
    IExtensionPoint extensionPoint =
        registry.getExtensionPoint(PerforceUIPlugin.ID, "preSubmitOperations");
    IConfigurationElement[] configurationElements = extensionPoint.getConfigurationElements();

    List<IPreSubmitOperation> result =
        Lists.newArrayListWithExpectedSize(configurationElements.length);

    for (IConfigurationElement element : configurationElements) {
      try {
        IPreSubmitOperation validator =
            (IPreSubmitOperation) element.createExecutableExtension("class");
        result.add(validator);
      } catch (CoreException e) {
        PerforceCorePlugin.log(e.getStatus());
      }
    }

    return result.toArray(new IPreSubmitOperation[0]);
  }


  /**
   * Convenience method for running an operation with progress and error
   * feedback.
   *
   * @param runnable the runnable which executes the operation
   * @param progressKind one of PROGRESS_BUSYCURSOR or PROGRESS_DIALOG
   * @param perforceTransaction indicates whether everything should run within a
   *        perforce cache transaction.
   */
  public final void run(Shell shell, final IRunnableWithProgress runnable, int progressKind,
      final boolean perforceTransaction) {
    final IRunnableWithProgress perforceRunnable =
        new WorkspaceModifyDelegatingOperation(new IRunnableWithProgress() {
          public void run(IProgressMonitor monitor) throws InvocationTargetException,
              InterruptedException {
            try {
              if (perforceTransaction) {
                PerforceSynchronizer.getInstance().beginOperation(null);
              }

              monitor.beginTask("", 1000);
              runnable.run(Policy.subMonitorFor(monitor, 750));
            } finally {
              if (perforceTransaction) {
                try {
                  PerforceSynchronizer.getInstance().endOperation(
                      Policy.subMonitorFor(monitor, 250));
                } catch (PerforceException e) {
                  throw new InvocationTargetException(e);
                }
              }
            }
          }
        });

    final Exception[] exceptions = new Exception[] {null};
    if (progressKind == PerforceAction.PROGRESS_BUSYCURSOR) {
      BusyIndicator.showWhile(Display.getCurrent(), new Runnable() {
        public void run() {
          try {
            perforceRunnable.run(Policy.monitorFor(null));
          } catch (InvocationTargetException e) {
            exceptions[0] = e;
          } catch (InterruptedException e) {
            exceptions[0] = null;
          }
        }
      });
    } else {
      try {
        new ProgressMonitorDialog(shell).run(true, true, perforceRunnable);
      } catch (InvocationTargetException e) {
        exceptions[0] = e;
      } catch (InterruptedException e) {
        exceptions[0] = null;
      }
    }

    if (exceptions[0] != null) {
      handle(exceptions[0]);
    }
  }

  /**
   * Handles exceptions that occur in Perforce model elements.
   *
   * @param t exception that should be handled.
   */
  public static void handle(Throwable t) {
    if (t instanceof InvocationTargetException) {
      t = ((InvocationTargetException) t).getTargetException();
    }

    IStatus error = null;
    if (t instanceof CoreException) {
      error = ((CoreException) t).getStatus();
    } else if (t instanceof TeamException) {
      error = ((TeamException) t).getStatus();
    } else {
      error = new Status(Status.ERROR, PerforceUIPlugin.ID, 1, Policy.bind("simpleInternal"), t); //$NON-NLS-1$
    }

    Shell shell = new Shell(Display.getDefault());

    if (error.getSeverity() == IStatus.INFO) {
      MessageDialog.openInformation(shell, Policy.bind("information"), //$NON-NLS-1$
          error.getMessage());
    } else {
      ErrorDialog.openError(shell, Policy.bind("exception"), //$NON-NLS-1$
          error.getMessage(), error, IStatus.ERROR);
    }

    shell.dispose();

    // Let's log non-team exceptions
    if (!(t instanceof TeamException)) {
      PerforceUIPlugin.log(error);
    }
  }

}
